/*
 * Copyright 2022 The Open Islands Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.openislands.oi.master.manager;

import org.apache.commons.lang3.time.DateUtils;
import org.openislands.oi.constant.Dict;
import org.openislands.oi.constant.MessageCodeEnum;
import org.openislands.oi.constant.NodeState;
import org.openislands.oi.dao.entity.NodeDO;
import org.openislands.oi.dao.mapper.NodeMapper;
import org.openislands.oi.error.LogicException;
import org.openislands.oi.manager.CommonNodeManager;
import org.openislands.oi.manager.CommonRouteManager;
import org.openislands.oi.manager.GeneralManager;
import org.openislands.oi.manager.union.UnionPipelineManager;
import org.openislands.oi.pojo.base.BeanBuilder;
import org.openislands.oi.pojo.dto.CommonResult;
import org.openislands.oi.pojo.dto.NodeDTO;
import org.openislands.oi.util.ValueCopyUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Objects;

@Service
@SuppressWarnings("unused")
public class MasterNodeManager {
    private static final Logger log = LoggerFactory.getLogger(MasterNodeManager.class);

    @Resource
    private CommonNodeManager commonNodeManager;

    @Resource
    private CommonRouteManager commonRouteManager;

    @Resource(type = UnionPipelineManager.class)
    private UnionPipelineManager unionPipelineManager;

    @Value(Dict.NODE_HEARTBEAT_CHECK_TIME_SECOND)
    private Integer nodeHeartbeatTime;

    public Boolean breakNode(NodeDTO nodeDTO) {
        CommonResult<Object> result = commonNodeManager.updateNodeState(nodeDTO, NodeState.BROKE);
        LogicException.isTrue(result.isSuccess(), result.getCode(), result.getMessage());
        return true;
    }

    public Boolean deleteNode(NodeDTO nodeDTO) {
        final String nodeId = nodeDTO.getNodeId();
        Boolean delete = commonNodeManager.getNodeManager().delete(
                uw -> uw.eq(NodeDO::getState, NodeState.BROKE).eq(NodeDO::getNodeId, nodeId)
        );
        LogicException.isTrue(delete, MessageCodeEnum.DATA_UPDATE_ERROR, "delete failed");
        return commonRouteManager.deleteRoute(nodeId);
    }

    public Boolean nodeVerify(NodeDTO nodeDTO) {
        return commonNodeManager.getNodeManager().updateById(
                BeanBuilder.create(new NodeDO()).set(NodeDO::setId, nodeDTO.getId())
                        .set(NodeDO::setState, NodeState.ONLINE).build()
        );
    }

    public Boolean nodeVerifyFinished(NodeDTO nodeDTO) {
        GeneralManager<NodeMapper, NodeDO> nodeManager = commonNodeManager.getNodeManager();
        final String updateNodeId = nodeDTO.getNodeId();
        final NodeDO nodeDO = nodeManager.getById(nodeDTO.getId());
        if (Objects.nonNull(nodeDO)) {
            try {
                // update node info
                final NodeDO updateDO = new NodeDO();
                updateDO.setId(nodeDTO.getId());
                Date date = new Date();
                updateDO.setJoinTime(date);
                updateDO.setLastHeartbeatTime(date);
                updateDO.setNodeId(updateNodeId);
                updateDO.setPublicKey(nodeDTO.getPublicKey());
                updateDO.setState(NodeState.ONLINE);
                nodeManager.updateById(updateDO);

                // merge update route
                nodeDTO.setNodeId(nodeDO.getNodeId());
                commonRouteManager.mergeRoute(nodeDTO, updateNodeId);
            } catch (DuplicateKeyException e) {
                // if the node exists, delete directly the temporary node data
                final NodeDTO deleteDTO = new NodeDTO();
                deleteDTO.setNodeId(nodeDO.getNodeId());
                unionPipelineManager.fire(CommonNodeManager.DELETE_NODE_AND_ROUTE, deleteDTO);
                log.warn("the node: {} exists. delete conflict node: {}. e: {}", updateNodeId, nodeDO.getNodeId(), e.getMessage());
                return false;
            }
        }

        return true;
    }

    public Boolean updateNodeOffline(NodeDTO updateDTO) {
        return updateNodeHeartbeatState(updateDTO, NodeState.OFFLINE, NodeState.ONLINE, NodeState.OFFLINE);
    }

    public Boolean updateNodeOnline(NodeDTO updateDTO) {
        return updateNodeHeartbeatState(updateDTO, NodeState.ONLINE, NodeState.ONLINE, NodeState.OFFLINE);
    }

    public Boolean updateNodeHeartbeatState(NodeDTO dto, NodeState newState, NodeState... oldState) {
        return commonNodeManager.getNodeManager().update(uw -> uw
                .set(NodeDO::getState, newState)
                .set(NodeDO::getLastHeartbeatTime, new Date())
                .eq(NodeDO::getEnabled, true)
                .eq(NodeDO::getNodeId, dto.getNodeId())
                .in(oldState.length > 0, NodeDO::getState, Arrays.asList(oldState))
        );
    }

    public List<NodeDTO> queryAnyListDTO(NodeDTO queryDTO) {
        return commonNodeManager.getNodeManager().queryAnyLikeDTO(queryDTO);
    }

    public List<NodeDTO> queryHeartbeatNode() {
        List<NodeDO> nodeDOList = commonNodeManager.getNodeManager().queryAnyList(
                uw -> uw.eq(NodeDO::getEnabled, true)
                        .lt(NodeDO::getLastHeartbeatTime, DateUtils.addSeconds(new Date(), -nodeHeartbeatTime))
                        .in(NodeDO::getState, NodeState.OFFLINE, NodeState.ONLINE)
        );
        return ValueCopyUtils.copyBuilderList(nodeDOList, builder -> builder.build(new NodeDTO()));
    }

    public List<NodeDTO> queryDeleteNode() {
        return commonNodeManager.getNodeManager().queryAnyLikeDTO(
                BeanBuilder.create(new NodeDTO()).set(NodeDTO::setState, NodeState.DELETED).build()
        );
    }
}
